<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="root">
    <div class="c1">
      <div title="tt1" id="id">{{ name }}</div>
      <div title="tt2">{{ age }}</div>
      <div title="tt3">{{ gender }}</div>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
    </div>
  </div>
  <script>

    /** 虚拟 DOM 构造函数 */
    class VNode {
      constructor(tag, data, value, type) {
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = [];
      }

      appendChild(vnode) {
        this.children.push(vnode);
      }
    }


    /** 由 HTML DOM -> VNode: 将这个函数当做 compiler 函数 */
    function getVNode(node) {
      let nodeType = node.nodeType;
      let _vnode = null;
      if (nodeType === 1) {
        // 元素
        let nodeName = node.nodeName;
        let attrs = node.attributes;
        let _attrObj = {};
        for (let i = 0; i < attrs.length; i++) { // attrs[ i ] 属性节点 ( nodeType == 2 )
          _attrObj[attrs[i].nodeName] = attrs[i].nodeValue;
        }
        _vnode = new VNode(nodeName, _attrObj, undefined, nodeType);

        // 考虑 node 的子元素
        let childNodes = node.childNodes;
        for (let i = 0; i < childNodes.length; i++) {
          _vnode.appendChild(getVNode(childNodes[i])); // 递归
        }

      } else if (nodeType === 3) {

        _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType);
      }

      return _vnode;
    }


    let rkuohao = /\{\{(.+?)\}\}/g;
    /** 根据路径 访问对象成员 */
    function getValueByPath(obj, path) {
      let paths = path.split('.'); // [ xxx, yyy, zzz ]
      let res = obj;
      let prop;
      while (prop = paths.shift()) {
        res = res[prop];
      }
      return res;
    }
    
    /** 将 带有 坑的 Vnode 与数据 data 结合, 得到 填充数据的 VNode: 模拟 AST -> VNode */
    function combine(vnode, data) {
      let _type = vnode.type;
      let _data = vnode.data;
      let _value = vnode.value;
      let _tag = vnode.tag;
      let _children = vnode.children;


      let _vnode = null;

      if (_type === 3) { // 文本节点 

        // 对文本处理
        _value = _value.replace(rkuohao, function (_, g) {
          return getValueByPath(data, g.trim());
        });

        _vnode = new VNode(_tag, _data, _value, _type)

      } else if (_type === 1) { // 元素节点
        _vnode = new VNode(_tag, _data, _value, _type);
        _children.forEach(_subvnode => _vnode.appendChild(combine(_subvnode, data)));
      }

      return _vnode;
    }


    function JGVue(options) {
      this._data = options.data;
      this._template = document.querySelector(options.el); // vue 是字符串, 这里是 DOM 

      this.mount(); // 挂载
    }

    JGVue.prototype.mount = function () {
      // 需要提供一个 render 方法: 生成 虚拟 DOM（render方法带有缓存功能）
      this.render = this.createRenderFn()

      this.mountComponent();
    }
    JGVue.prototype.mountComponent = function () {
      // 执行 mountComponent() 函数 
      let mount = () => {
        this.update(this.render())
      }
      mount.call(this); // 本质应该交给 watcher 来调用, 但是还没有讲到这里
    }

    /**
     * 在真正的 Vue 中使用了 二次提交的 设计结构
     * 1. 在 页面中 的 DOM 和 虚拟 DOM 是一一对应的关系
     * 2. 先 由 AST 和 数据 生成 VNode ( 新, render函数来实现的 )
     * 3. 将 旧的 VNode 和 新的 VNode 比较 ( diff ), 更新 ( update )
     */

    // 这里是生成 render 函数, 目的是缓存 抽象语法树 ( 我们使用 虚拟 DOM 来模拟 )
    JGVue.prototype.createRenderFn = function () {
      let ast = getVNode(this._template);
      // Vue: 将 AST + data => VNode
      // 我们: 带有坑的 VNode + data => 含有数据的 VNode（相当于是模拟）
      return function render() {
        // 将 带有 坑的 VNode 转换为 带数据的 VNode
        let _tmp = combine(ast, this._data);
        debugger;
        return _tmp;
      }
    }

    // 将虚拟 DOM 渲染到页面中: diff 算法就在里
    JGVue.prototype.update = function () {
      // 简化, 直接生成 HTML DOM replaceChild 到页面中

    }




    let app = new JGVue({
      el: '#root',
      data: {
        name: '张三'
        , age: 19
      }
    });

    app.name = '李四'; // 这个赋值已完成, 页面数据就更新

  </script>
</body>

</html>